{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Lmfit Model Example\n",
    "\n",
    "This notebook shows a simple example of using [``lmfit.Model``](http://lmfit.github.io/lmfit-py/model.html#the-model-class)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Preliminary imports:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "%matplotlib inline\n",
    "#%config InlineBackend.figure_format='retina'  # for hi-dpi displays"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``Model`` class is a flexible, concise curve fitter. I will illustrate fitting example data to an exponential decay."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def decay(t, N, tau):\n",
    "    return N*np.exp(-t/tau)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The parameters are in no particular order. We'll need some example data. I will use ``N=7`` and ``tau=3``, and I'll add a little noise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = np.linspace(0, 5, num=1000)\n",
    "data = decay(t, 7, 3) + np.random.randn(*t.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Simplest Usage"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from lmfit import Model\n",
    "\n",
    "model = Model(decay, independent_vars=['t'])\n",
    "result = model.fit(data, t=t, N=10, tau=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Model infers the parameter names by inspecting the arguments of the function, ``decay``. Then I passed the independent variable, ``t``, and initial guesses for each parameter. A residual function is automatically defined, and a least-squared regression is performed.\n",
    "\n",
    "We can immediately see the best-fit values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result.values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and easily pass those to the original model function for plotting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig = result.plot(fig_kws={'figsize': (8, 7)})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can review the best-fit `Parameters` accessing `result.params`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result.params.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "More information about the fit is stored in the result, which is an [``lmfit.MimimizerResult``](https://lmfit.github.io/lmfit-py/fitting.html#lmfit.minimizer.MinimizerResult) object."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Specifying Bounds and Holding Parameters Constant"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Above, the ``Model`` class implicitly builds ``Parameter`` objects from keyword arguments of ``fit`` that match the argments of ``decay``. You can build the ``Parameter`` objects explicity; the following is equivalent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from lmfit import Parameter\n",
    "\n",
    "result = model.fit(data, t=t, \n",
    "                   N=Parameter(value=10), \n",
    "                   tau=Parameter(value=1))\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By building ``Parameter`` objects explicitly, you can specify bounds (``min``, ``max``) and set parameters constant (``vary=False``)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = model.fit(data, t=t, \n",
    "                   N=Parameter(value=7, vary=False), \n",
    "                   tau=Parameter(value=1, min=0))\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Defining Parameters in Advance"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Passing parameters to ``fit`` can become unwieldly. As an alternative, you can extract the parameters from ``model`` like so, set them individually, and pass them to ``fit``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = model.make_params()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params['N'].value = 10  # initial guess\n",
    "params['tau'].value = 1\n",
    "params['tau'].min = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = model.fit(data, params, t=t)\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Keyword arguments override ``params``, resetting ``value`` and all other properties (``min``, ``max``, ``vary``)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = model.fit(data, params, t=t, tau=1)\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The input parameters are not modified by ``fit``. They can be reused, retaining the same initial value. If you want to use the result of one fit as the initial guess for the next, simply pass ``params=result.params``."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### A Helpful Exception"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "All this implicit magic makes it very easy for the user to neglect to set a parameter. The ``fit`` function checks for this and raises a helpful exception."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = model.fit(data, t=t, tau=1)  # N unspecified"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "An *extra* parameter that cannot be matched to the model function will throw a ``UserWarning``, but it will not raise, leaving open the possibility of unforeseen extensions calling for some parameters."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Weighted Fits"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use the ``sigma`` argument to perform a weighted fit. If you prefer to think of the fit in term of ``weights``, ``sigma=1/weights``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weights = np.arange(len(data))\n",
    "result = model.fit(data, params, t=t, weights=weights)\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Handling Missing Data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By default, attemping to fit data that includes a ``NaN``, which conventionally indicates a \"missing\" observation, raises a lengthy exception. You can choose to ``omit`` (i.e., skip over) missing values instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_with_holes = data.copy()\n",
    "data_with_holes[[5, 500, 700]] = np.nan  # Replace arbitrary values with NaN.\n",
    "\n",
    "model = Model(decay, independent_vars=['t'], nan_policy='omit')\n",
    "result = model.fit(data_with_holes, params, t=t)\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you don't want to ignore missing values, you can set the model to raise proactively, checking for missing values before attempting the fit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Uncomment to see the error\n",
    "#model = Model(decay, independent_vars=['t'], nan_policy='raise')\n",
    "#result = model.fit(data_with_holes, params, t=t)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The default setting is ``nan_policy='raise'``, which does check for NaNs and raises an exception when present.\n",
    "\n",
    "Null-chekcing  relies on ``pandas.isnull`` if it is available. If pandas cannot be imported, it silently falls back on ``numpy.isnan``."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Data Alignment"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Imagine a collection of time series data with different lengths. It would be convenient to define one sufficiently long array ``t`` and use it for each time series, regardless of length. The [``pandas``](http://pandas.pydata.org/pandas-docs/stable/) provides tools for aligning indexed data. And, unlike most wrappers to ``scipy.leastsq``, ``Model`` can handle pandas objects out of the box, using its data alignment features.\n",
    "\n",
    "Here I take just a slice of the ``data`` and fit it to the full ``t``. It is automatically aligned to the correct section of ``t`` using Series' index."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pandas import Series\n",
    "\n",
    "model = Model(decay, independent_vars=['t'])\n",
    "truncated_data = Series(data)[200:800]  # data points 200-800\n",
    "t = Series(t)  # all 1000 points\n",
    "result = model.fit(truncated_data, params, t=t)\n",
    "result.params"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Data with missing entries and an unequal length still aligns properly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = Model(decay, independent_vars=['t'], nan_policy='omit')\n",
    "truncated_data_with_holes = Series(data_with_holes)[200:800]\n",
    "result = model.fit(truncated_data_with_holes, params, t=t)\n",
    "result.params"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
